havce CTF Team

havce CTF Team

HeroCTF 2024 - buafflet

Photo of Lorenzo Ferrari
pwn

Description

You have one bullet, use it wisely…

Files index

  • buafllet.ko
  • config
  • Image
  • initramfs.cpio.gz
  • run.sh

Overview Kernel Module

The kernel module, seen from the perspective of the IDA decompiler.

__int64 __fastcall buafllet_ioctl(file *fp, __int64 cmd, unsigned __int64 arg)
{
  __int64 v3; // x1
  char *v5; // x19
  unsigned __int64 v6; // x0
  unsigned __int64 v7; // x1
  size_t v8; // x0
  unsigned __int64 StatusReg; // x0
  unsigned __int64 v10; // x0
  __int64 v11; // x3

  if ( (_DWORD)cmd == 18 )
  {
    if ( bullet )
    {
      StatusReg = _ReadStatusReg(ARM64_SYSREG(3, 0, 4, 1, 0));
      if ( (*(_DWORD *)(StatusReg + 44) & 0x200000) != 0
        || (v11 = *(_QWORD *)StatusReg, v10 = arg, (v11 & 0x4000000) != 0) )
      {
        v10 = ((__int64)(arg << 8) >> 8) & arg;
      }
      if ( v10 > 0xFFFFFFFFFC00LL )
        return -14LL;
      if ( _arch_copy_to_user(arg & 0xFF7FFFFFFFFFFFFFLL) )
        return -14LL;
    }
    return 0LL;
  }
  if ( (unsigned int)cmd > 0x12 )
  {
    if ( (_DWORD)cmd == 19 )
    {
      v5 = bullet;
      if ( bullet )
      {
        v6 = _ReadStatusReg(ARM64_SYSREG(3, 0, 4, 1, 0));
        v7 = arg;
        if ( ((_DWORD *)(v6 + 44) & 0x200000) != 0 || ((_QWORD *)v6 & 0x4000000) != 0 )
          arg &= (__int64)(arg << 8) >> 8;
        if ( arg > 0xFFFFFFFFFC00LL )
        {
          v8 = 1024LL;
LABEL_13:
          memset(v5, 0, v8);
          return -14LL;
        }
        v8 = _arch_copy_from_user(v5, v7 & 0xFF7FFFFFFFFFFFFFLL, 1024LL);
        if ( v8 )
        {
          v5 = &v5[-v8 + 1024];
          goto LABEL_13;
        }
      }
    }
    return 0LL;
  }
  if ( (_DWORD)cmd == 16 )
    return ioctl_get_bullet(arg);
  if ( (_DWORD)cmd != 17 )
    return 0LL;
  mutex_lock(&g_mutex, cmd, arg);
  if ( bullet_used )
  {
    kfree(bullet);
    mutex_unlock(&g_mutex, v3);
    printk("Now what you gonna do?");
    return 0LL;
  }
  else
  {
    mutex_unlock(&g_mutex, &bullet_used);
    return -22LL;
  }

The module implements 4 different ioctls.

CMD_ALLOC (0x10)

Creates a single allocation via kzalloc and then sets the global variable bullet_used to 1. The size can vary between 0x490 and 0x3000.

CMD_FREE (0x11)

Frees the previously allocated chunk. This ioctl contains a bug: we can free the chunk multiple times, since the pointer is not cleared. Easy Use After Free :)

CMD_READ (0x12)

Reads the content of our allocation. This gets us a UAF read primitive.

CMD_WRITE (0x13)

Edits the content of the chunk we have created. This gets us a UAF write primitive. That means we have both r/w UAF primitives :D.

This challenge has quite a simple vulnerability with really powerful primitives. However the kernel is compiled with some annoying mitigations enabled.

CONFIG_SLUB=y
CONFIG_SLAB_MERGE_DEFAULT=n
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_RANDOM_KMALLOC_CACHES=y

CONFIG_USERFAULTFD=n
CONFIG_FUSE_FS=n

In particular, CONFIG_RANDOM_KMALLOC_CACHES=y creates multiple kmalloc caches for the same size! When making an allocation with kmalloc, an appropriate cache (for our size) is selected from the available ones, using a secret random key (generated at boot) and the address of the call site from which kmalloc was invoked, thus making it basically impossible to guess which cache will be selected for our allocation. Even two identical objects may not be allocated in the same cache if they are allocated in different functions!

Exploitation Overview

There are two ways to exploit this challenge:

  1. Control Flow hijacking
  2. Data Only

We opted for the second solution, because it seemed easier to solve it that way. The technique we used some sort of cross caching attack. We create an allocation greater than 8k bytes, so that kmalloc will allocate chunk of memory just for our object, partially bypassing the random caches mitigation.

		if (size > KMALLOC_MAX_CACHE_SIZE) /* KMALLOC_MAX_CACHE_SIZE is 0x2000 (or 8k bytes) */
			return kmalloc_large_node_noprof(size, flags, node);

The exploitation flow goes as follows:

  1. Create the allocation larger than 8k bytes via the CMD_ALLOC ioctl.

image

  1. Free the allocation via the CMD_FREE ioctl, so that the kernel will return the pages used by our chunk back to the page allocator.

image

  1. Now we use the capset syscall in combination with io_uring’s personalities, to spray a f*ck ton of cred structs (NOTE: The cred struct is the data structure that is used to manage access permissions). An io_uring personality is a set of permission we can use when making operations, such as opening a file, with io_uring. To spam creds, what we did is:
    • Allocate a cred struct using capset.
    • Register it as a personality for use with io_uring.
    • Optionally, store the ID of the personality somewhere, we’ll need it later (optional since the ID can be guessed).

image

  1. Keep checking if we managed to get a cache collision by using the CMD_READ ioctl.

  2. When we get a collision, we overwrite the cred structs to get root privileges (by setting all the *uid and *gid fields to 0).

  3. We try to open flag.txt by using the IDs of the io_uring personalities we registered earlier, until we find one that open the file!

image

This was the first time we used this method. Most of the io_uring code we literally copied from here.

You can find the commented exploit here.

Final considerations

I really enjoyed this challenge. The vulnerability was pretty easy, but the exploitation teqnique was kinda hard for me, since I’m new to kernel pwning. Also shout out to my friend markx86 that managed to solve it on time :)

Feel free to contact us if anything is unclear or if I missed something I’m open to any tips!